<?php

namespace Xin\Support\Traits;

use BadMethodCallException;
use Closure;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;

trait Macroable
{

	/**
	 * The registered string macros.
	 *
	 * @var array
	 */
	protected static $macros = [];

	/**
	 * Mix another object into the class.
	 *
	 * @param object $mixin
	 * @param bool $replace
	 * @return void
	 * @throws ReflectionException
	 */
	public static function mixin($mixin, bool $replace = true)
	{
		$methods = (new ReflectionClass($mixin))->getMethods(
			ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
		);

		foreach ($methods as $method) {
			if ($replace || !static::hasMacro($method->name)) {
				$method->setAccessible(true);
				static::macro($method->name, $method->invoke($mixin));
			}
		}
	}

	/**
	 * Checks if macro is registered.
	 *
	 * @param string $name
	 * @return bool
	 */
	public static function hasMacro(string $name)
	{
		return isset(static::$macros[$name]);
	}

	/**
	 * Register a custom macro.
	 *
	 * @param string $name
	 * @param object|callable $macro
	 * @return void
	 */
	public static function macro(string $name, $macro)
	{
		static::$macros[$name] = $macro;
	}

	/**
	 * Dynamically handle calls to the class.
	 *
	 * @param string $method
	 * @param array $parameters
	 * @return mixed
	 * @throws BadMethodCallException
	 */
	public static function __callStatic(string $method, array $parameters)
	{
		if (!static::hasMacro($method)) {
			throw new BadMethodCallException(sprintf(
				'Method %s::%s does not exist.', static::class, $method
			));
		}

		$macro = static::$macros[$method];

		if ($macro instanceof Closure) {
			return call_user_func_array(Closure::bind($macro, null, static::class), $parameters);
		}

		return $macro(...$parameters);
	}

	/**
	 * Dynamically handle calls to the class.
	 *
	 * @param string $method
	 * @param array $parameters
	 * @return mixed
	 * @throws BadMethodCallException
	 */
	public function __call(string $method, array $parameters)
	{
		if (!static::hasMacro($method)) {
			throw new BadMethodCallException(sprintf(
				'Method %s::%s does not exist.', static::class, $method
			));
		}

		$macro = static::$macros[$method];

		if ($macro instanceof Closure) {
			return call_user_func_array($macro->bindTo($this, static::class), $parameters);
		}

		return $macro(...$parameters);
	}

}
